﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Collections.Concurrent;
using Sustainalytics.Entities.Governance;
using Sustainalytics.Entities;
using Sustainalytics.Utils;
using SimpleInjector;

namespace Sustainalytics.Governance.Computing
{
    public class GovernanceComputationEngine
    {
        private static GovernanceDataManager _dataManager;
        private static ConcurrentQueue<string> _log;
        protected Container SimpleInjectorContainer;

        public List<string> Log { get { return _log.ToList(); }}
        
        public GovernanceComputationEngine(GovernanceDataManager dataManager)
        {
            _dataManager = dataManager;
            _log = new ConcurrentQueue<string>();
        }


        public void Compute()
        {
            // WARNING! does not delete calculation methods! Their model is unique!
            //var computeResults = ComputeRatings();

            _dataManager.LoadIndicatorTemplates();

            var computeResults = GetImportedComputedRatings();

            var computationResults = ProcessBenchmarks(computeResults);

            _dataManager.SaveComputationResults(computationResults);
        }


        private ICollection<GovernanceComputeResult> GetImportedComputedRatings()
        {
            _dataManager.LoadComputationResults();

            var results = new ConcurrentBag<GovernanceComputeResult>();

            Parallel.ForEach(_dataManager.CompanyProfiles,
                new ParallelOptions
                {
                    MaxDegreeOfParallelism = Convert.ToInt32(Math.Ceiling(Environment.ProcessorCount * 0.75))
                },
                companyProfile =>
                {
                    try
                    {
                        var computationResult = _dataManager.ImportedComputationResults.ContainsKey(companyProfile.Id)
                            ? _dataManager.ImportedComputationResults[companyProfile.Id]
                            : null;

                        if (computationResult != null)
                        {
                            var result = new GovernanceComputeResult()
                            {
                                CompanyId = companyProfile.Id,
                                CompanyName = companyProfile.CompanyName,

                                RegionCode = companyProfile.RegionCode,
                                PeerGroup = companyProfile.Industry,
                                MarketCapitalization = companyProfile.MarketCapitalization,

                                Assessment = computationResult.OverallPerformance.Assessment,
                                Score = computationResult.OverallPerformance.Score,
                                Weight = computationResult.OverallPerformance.Weight,
                                WeightedScore = computationResult.OverallPerformance.WeightedScore,

                                IssueComputeResults = computationResult.IssuesPerformance
                                    .Select(x => new GovernanceIssueComputeResult()
                                    {
                                        Name = x.Name,
                                        Number = x.Number,

                                        CompanyId = companyProfile.Id,
                                        Region = companyProfile.RegionCode,
                                        PeerGroup = companyProfile.Industry,

                                        Assessment = x.Assessment,
                                        Score = x.Score,
                                        Weight = x.Weight,
                                        WeightedScore = x.WeightedScore
                                    })
                                    .ToList()
                            };

                            results.Add(result);
                        }
                    }
                    catch (Exception ex)
                    {
                        _log.Enqueue($"Failed to compute ratings for company id:{companyProfile.Id}. Exception: {ex.ToString()}");
                    }
                });

            return results.ToList();
        }

        private ICollection<GovernanceComputeResult> ComputeRatings()
        {

            var sw = Stopwatch.StartNew();

            _dataManager.GetData();

            _log.Enqueue($"No of companies {_dataManager.CompanyProfiles.Count}");

            var results = new ConcurrentBag<GovernanceComputeResult>();
            var researchInProgressCompanyProfiles = new ConcurrentBag<GovernanceCompanyProfile>();

            Parallel.ForEach(_dataManager.CompanyProfiles,
                new ParallelOptions
                {
                    MaxDegreeOfParallelism = Convert.ToInt32(Math.Ceiling(Environment.ProcessorCount * 0.75))
                },
                companyProfile =>
                {
                    try
                    {
                        var issueWeights = _dataManager.RegionIssueWeights.ContainsKey(companyProfile.RegionCode)
                            ? _dataManager.RegionIssueWeights[companyProfile.RegionCode]
                            : null;

                        var indicators = _dataManager.Indicators.ContainsKey(companyProfile.Id)
                            ? _dataManager.Indicators[companyProfile.Id]
                            : null;

                        var indicatorWeights = _dataManager.RegionIndicatorWeights.ContainsKey(companyProfile.RegionCode)
                            ? _dataManager.RegionIndicatorWeights[companyProfile.RegionCode]
                            : null;

                        var importedIssuePerformances = _dataManager.ImportedIssuePerformances.ContainsKey(companyProfile.Id)
                            ? _dataManager.ImportedIssuePerformances[companyProfile.Id]
                            : null;

                        var issueComputeResults = new List<GovernanceIssueComputeResult>();

                        double finalScore = 0;
                        double finalWeight = 0;

                        foreach (var issue in _dataManager.IssueTemplates)
                        {
                            double issueScore = 0;
                            double issueWeight = 0;
                            double denormalizedIssueScore = 0;
                            double weightedScore = 0;
                            double totalOverlayScore = 0;


                            if (_dataManager.ImportedIssues.Exists(x => x == issue.IssueNumber))
                            {
                                if (importedIssuePerformances == null || !importedIssuePerformances.ContainsKey(issue.IssueNumber))
                                {
                                    researchInProgressCompanyProfiles.Add(companyProfile);
                                    return;
                                }

                                var importedIssuePerformance = importedIssuePerformances[issue.IssueNumber];

                                denormalizedIssueScore = importedIssuePerformance.Score;

                                issueWeight = issueWeights.ContainsKey(issue.IssueNumber)
                                    ? issueWeights[issue.IssueNumber]
                                    : float.MinValue;

                                if (issueWeight == float.MinValue)
                                    throw new Exception($"There is no issue weight, for company id: {companyProfile.Id}!");

                                weightedScore = denormalizedIssueScore * issueWeight;
                            }
                            else
                            {

                                var weights = indicatorWeights.Where(i => i.Key.Contains(issue.IssueNumber)).ToList();

                                foreach (var indicatorWeight in weights)
                                {
                                    var rawScore = 0;

                                    if (indicators != null)
                                    {
                                        rawScore = indicators.ContainsKey(indicatorWeight.Key)
                                            ? indicators[indicatorWeight.Key]
                                            : int.MinValue;

                                        if (rawScore == int.MinValue)
                                            throw new Exception("There is no indicator score!");
                                    }

                                    double normalizedScore = NormalizeRawScore(rawScore);

                                    double indicatorRating = indicatorWeight.Value * normalizedScore;

                                    issueScore += indicatorRating;
                                    issueWeight += indicatorWeight.Value;


                                    var correctionFactors = _dataManager.CorrectionFactors.ContainsKey(indicatorWeight.Key)
                                        ? _dataManager.CorrectionFactors[indicatorWeight.Key]
                                        : null;

                                    if (correctionFactors != null)
                                    {
                                        foreach (var correctionFactor in correctionFactors)
                                        {
                                            if (correctionFactor.Value != 0)
                                            {
                                                double correctionWeight = correctionFactor.Value;

                                                var correctionRawScore = 0;

                                                if (indicators != null)
                                                {
                                                    correctionRawScore = indicators.ContainsKey(correctionFactor.Key)
                                                        ? indicators[correctionFactor.Key]
                                                        : 0;
                                                }

                                                double correctionScore = NormalizeRawScore(correctionRawScore);


                                                var overlayScore = (normalizedScore + correctionScore) * Math.Abs(normalizedScore * correctionScore) * correctionWeight;

                                                totalOverlayScore += overlayScore;
                                            }
                                        }
                                    }
                                }

                                var totalIssueScore = issueScore + totalOverlayScore / issueWeight;

                                denormalizedIssueScore = DenormalizeRawScore(totalIssueScore);

                                issueWeight = issueWeights.ContainsKey(issue.IssueNumber)
                                    ? issueWeights[issue.IssueNumber]
                                    : float.MinValue;

                                if (issueWeight == float.MinValue)
                                    throw new Exception($"There is no issue weight, for company id: {companyProfile.Id}!");

                                weightedScore = denormalizedIssueScore * issueWeight;
                            }


                            var issueComputeResult = new GovernanceIssueComputeResult()
                            {
                                Name = issue.IssueName,
                                Number = issue.IssueNumber,

                                CompanyId = companyProfile.Id,
                                Region = companyProfile.RegionCode,
                                PeerGroup = companyProfile.Industry,

                                Score = denormalizedIssueScore,
                                Weight = issueWeight,
                                WeightedScore = weightedScore
                            };

                            issueComputeResults.Add(issueComputeResult);

                            finalScore += weightedScore;
                            finalWeight += issueWeight;
                        }

                        var totalScore = finalScore / finalWeight;

                        var overallPerformance = new GovernanceOverallPerformance()
                        {
                            Score = totalScore,
                            Weight = finalWeight,
                            WeightedScore = totalScore,
                            Performances = new List<GovernancePerformance>()
                        };

                        var result = new GovernanceComputeResult()
                        {
                            CompanyId = companyProfile.Id,
                            CompanyName = companyProfile.CompanyName,

                            RegionCode = companyProfile.RegionCode,
                            PeerGroup = companyProfile.Industry,
                            MarketCapitalization = companyProfile.MarketCapitalization,

                            Score = totalScore,
                            Weight = finalWeight,
                            WeightedScore = totalScore,

                            IssueComputeResults = issueComputeResults
                        };

                        results.Add(result);
                    }
                    catch (Exception ex)
                    {
                        researchInProgressCompanyProfiles.Add(companyProfile);

                        _log.Enqueue($"Failed to compute ratings for company id:{companyProfile.Id}. Exception: {ex.ToString()}");
                    }
                });

            _log.Enqueue($"No of computation results {results.Count}");

            if (researchInProgressCompanyProfiles.Any())
            {
                var companyProfiles = researchInProgressCompanyProfiles.ToList();

                _log.Enqueue($"{companyProfiles.Count} company profiles updated to research in progress.");

                companyProfiles.ForEach(cp => _log.Enqueue($"The company {cp.CompanyName} ({cp.Id}) was updated to research in progress"));

                var companyIds = companyProfiles.Select(a => a.Id).ToList();

                _dataManager.UpdateGovernanceResearchFlagInCorporateData(companyIds);
                _dataManager.CleanupRawData(companyIds);
            }

            sw.Stop();

            _log.Enqueue($"Computation completed. Elapsed time: {sw.Elapsed}");


            return results.ToList();
        }

        private double NormalizeRawScore(double score)
        {
            return (score - 50) / 50;
        }

        private double DenormalizeRawScore(double score)
        {
            double denormalized = (score * 50 + 50);
            double cappedScore;
            cappedScore = (denormalized < 0) ? 0 : denormalized;
            cappedScore = (cappedScore > 100) ? 100 : cappedScore;

            return cappedScore;
        }

        private AssessmentType GetAssesmentType(double score, string issue, Dictionary<string, List<int>> assesmentThresholds)
        {
            var assesmentTypeCount = Enum.GetNames(typeof(AssessmentType)).Count();
            var roundedScore = ScoreConversion.GetRoundedScoreToEven(score);
            //var roundedScore = Math.Round(score, 0);

            if (assesmentThresholds.ContainsKey(issue))
            {
                var thresholds = assesmentThresholds[issue].OrderByDescending(x => x).ToList();

                if (thresholds.Count() + 1 != assesmentTypeCount)
                {
                    throw new ApplicationException(String.Format("Thresholds for issue '{0}' should be {1}", issue, assesmentTypeCount - 1));
                }

                for (int i = 0; i < thresholds.Count(); i++)
                {
                    var threshold = thresholds[i];
                    if (roundedScore >= threshold)
                    {
                        return (AssessmentType)i;
                    }
                }

                return AssessmentType.Industry_Laggard;
            }
            else
            {
                throw new ApplicationException(String.Format("Cannot find thresholds for issue '{0}'", issue));
            }
        }


        private IEnumerable<GovernanceComputationResult> ProcessBenchmarks(ICollection<GovernanceComputeResult> computeResults)
        {
            var sw = Stopwatch.StartNew();


            _log.Enqueue($"Start Benchmarking with {computeResults.Count} compute results");


            Dictionary<Guid, GovernancePerformance> peerGroupBenchmark = null;
            Dictionary<Guid, GovernancePerformance> regionBenchmark = null;
            var issuesBenchmarks = new ConcurrentDictionary<KeyValuePair<string, Guid>, List<GovernancePerformance>>();
            Dictionary<Guid, GovernanceRelativePerformance> relativePerformances = null;


            var actions = new List<Action>
            {
                () => peerGroupBenchmark = GetComparativeGroupBenchmark(PerformanceType.PeerGroup, computeResults),
                () => regionBenchmark = GetComparativeGroupBenchmark(PerformanceType.Region, computeResults),
            };

            foreach (var issue in _dataManager.IssueTemplates)
            {
                actions.Add(() =>
                {
                    var issueComputeResults = computeResults.SelectMany(x => x.IssueComputeResults)
                        .Where(x => x.Number == issue.IssueNumber).ToList();

                    var issueBenchmarks = GetIssueBenchmarks(issue.IssueNumber, issueComputeResults);

                    foreach (var issueBenchmark in issueBenchmarks)
                        issuesBenchmarks.TryAdd(issueBenchmark.Key, issueBenchmark.Value);
                });
            }

            actions.RunInParallel();

            relativePerformances = GetRelativePerformances(computeResults, regionBenchmark, peerGroupBenchmark);


            var exceptions = new ConcurrentQueue<Exception>();
            var results = new ConcurrentBag<GovernanceComputationResult>();


            Parallel.ForEach(computeResults,
                new ParallelOptions
                {
                    MaxDegreeOfParallelism = Convert.ToInt32(Math.Ceiling(Environment.ProcessorCount * 0.75))
                },
                computeResult =>
                {
                    try
                    {
                        var overallPerformance = new GovernanceOverallPerformance()
                        {
                            Score = computeResult.Score,
                            Assessment = computeResult.Assessment,
                            //Assessment = GetAssesmentType(computeResult.Score, "CG.0", _dataManager.AssesmentThresholds),
                            Weight = computeResult.Weight,
                            WeightedScore = computeResult.WeightedScore,
                            Performances = new List<GovernancePerformance>()
                        };

                        overallPerformance.Performances.Add(regionBenchmark[computeResult.CompanyId]);
                        overallPerformance.Performances.Add(peerGroupBenchmark[computeResult.CompanyId]);

                        var relativePerformance = relativePerformances[computeResult.CompanyId];

                        var result = new GovernanceComputationResult()
                        {
                            Id = computeResult.CompanyId,

                            OverallPerformance = overallPerformance,
                            RelativePerformance = relativePerformance,
                            IssuesPerformance = new List<GovernanceIssuePerformance>()
                        };

                        foreach (var issueComputeResult in computeResult.IssueComputeResults)
                        {
                            var key = new KeyValuePair<string, Guid>(issueComputeResult.Number, computeResult.CompanyId);
                            var performances = issuesBenchmarks[key];

                            result.IssuesPerformance.Add(new GovernanceIssuePerformance
                            {
                                Name = issueComputeResult.Name,
                                Number = issueComputeResult.Number,
                                Score = issueComputeResult.Score,
                                Assessment = issueComputeResult.Assessment,
                                //Assessment = GetAssesmentType(issueComputeResult.Score, issueComputeResult.Number, _dataManager.AssesmentThresholds),
                                Weight = issueComputeResult.Weight,
                                WeightedScore = issueComputeResult.WeightedScore,
                                Performances = performances
                            });
                        }

                        results.Add(result);
                    }
                    catch (Exception ex)
                    {
                        exceptions.Enqueue(ex);
                        _log.Enqueue($"Failed to benchmark company with id:{computeResult.CompanyId}. Exception: {ex.ToString()}");
                    }
                });

            if (exceptions.Count > 0)
                throw new AggregateException(exceptions);


            sw.Stop();

            _log.Enqueue($"Benchmarking completed. Elapsed time: {sw.Elapsed}");


            return results.ToList();
        }

        private Dictionary<Guid, GovernanceRelativePerformance> GetRelativePerformances(ICollection<GovernanceComputeResult> computeResults, Dictionary<Guid, GovernancePerformance> regionBenchmarks, Dictionary<Guid, GovernancePerformance> peerGroupBenchmarks)
        {
            //GetCompanies();
            //var compRatings = from cp in _companies
            //                  join cr in companyRatings on cp.Id equals cr.CompanyId
            //                  select new
            //                  {
            //                      cp.MarketCapitalization,
            //                      cr.CompanyId,
            //                      cp.CompanyName,
            //                      Score = cr.Scores.Any() ? cr.Scores[0].Score : (float)0,
            //                      RegionName = cp.RegionCode,
            //                      Rating = cr,
            //                      PeerGroup = cp.Industry
            //                  };

            var relativePerformances = new Dictionary<Guid, GovernanceRelativePerformance>();

 

            foreach (var computeResult in computeResults)
            {
                computeResult.RegionCount = regionBenchmarks[computeResult.CompanyId].GroupCount;

                var peerGroupBenchmark = peerGroupBenchmarks[computeResult.CompanyId];

                computeResult.PeerGroupRank = peerGroupBenchmark.Rank;
                computeResult.PeerGroupCount = peerGroupBenchmark.GroupCount;

            }

            var compRatingsPerPeerGroup = computeResults.GroupBy(x => x.PeerGroup).ToDictionary(c => c.Key, c => c.ToList());

            //var peerGroupTop5Dict = collection.GroupBy(c => c.PeerGroup)
            //    .ToDictionary(c => c.Key, c => c.Where(x => x.Rating.CompanyBenchmarks[PerformanceType.PeerGroup].Benchmarks[0].IssueBenchmark.Rank <= 5)
            //        .Select(cr => new GovernanceCompanyRating()
            //        {
            //            CompanyId = cr.CompanyId,
            //            Score = cr.Score,
            //            CompanyName = cr.CompanyName,
            //            Rank = cr.Rating.CompanyBenchmarks[PerformanceType.PeerGroup].Benchmarks[0].IssueBenchmark.Rank
            //        })
            //        .OrderBy(cr => cr.Rank)
            //        .ThenBy(cr => cr.CompanyName)
            //        .Take(5)
            //        .ToList());

            var peerGroupTop5Dict = computeResults.GroupBy(c => c.PeerGroup)
                .ToDictionary(c => c.Key, c => c.Where(x => x.PeerGroupRank <= 5)
                    .Select(cr => new GovernanceCompanyRating()
                    {
                        CompanyId = cr.CompanyId,
                        Score = (float)cr.Score,
                        CompanyName = cr.CompanyName,
                        Rank = cr.PeerGroupRank
                    })
                    .OrderBy(cr => cr.Rank)
                    .ThenBy(cr => cr.CompanyName)
                    .Take(5)
                    .ToList());

            compRatingsPerPeerGroup.Keys.ToList().ForEach(peerGroup =>
            {
                var compRatingsOrdered = compRatingsPerPeerGroup[peerGroup]
                    .OrderByDescending(x => x.MarketCapitalization)
                    .ToList();

                int i = 0;

                compRatingsOrdered.ForEach(comp =>
                {
                    int start, end;

                    GetNeighborsIndexes(i, compRatingsOrdered.Count(), out start, out end);

                    var peersList = new List<GovernanceCompanyRating>();

                    var neighborsRatings = compRatingsOrdered.GetRange(start, end - start + 1);

                    var governanceRelativePerformance = new GovernanceRelativePerformance();

                    governanceRelativePerformance.MarketCapRangeFrom = double.MaxValue;
                    governanceRelativePerformance.MarketCapRangeTo = 0;

                    neighborsRatings.ForEach(c =>
                    {
                        peersList.Add(new GovernanceCompanyRating()
                        {
                            CompanyId = c.CompanyId,
                            CompanyName = c.CompanyName,
                            Score = (float)c.Score,
                            //Rank = c.Rating.CompanyBenchmarks[PerformanceType.PeerGroup].Benchmarks[0].IssueBenchmark.Rank
                            Rank = c.PeerGroupRank
                        });
                        governanceRelativePerformance.MarketCapRangeFrom = Math.Min(c.MarketCapitalization, governanceRelativePerformance.MarketCapRangeFrom);
                        governanceRelativePerformance.MarketCapRangeTo = Math.Max(c.MarketCapitalization, governanceRelativePerformance.MarketCapRangeTo);
                    });

                    governanceRelativePerformance.PeersList = peersList.OrderByDescending(c => c.Score).ToArray();
                    //governanceRelativePerformance.PeerGroupCount = comp.Rating.CompanyBenchmarks[PerformanceType.PeerGroup].Benchmarks[0].IssueBenchmark.Count;
                    governanceRelativePerformance.PeerGroupCount = comp.PeerGroupCount;
                    //governanceRelativePerformance.RegionCount = comp.Rating.CompanyBenchmarks[PerformanceType.Region].Benchmarks[0].IssueBenchmark.Count;
                    governanceRelativePerformance.RegionCount = comp.RegionCount;
                    governanceRelativePerformance.Top5List = peerGroupTop5Dict[comp.PeerGroup].ToArray();



                    relativePerformances.Add(comp.CompanyId, governanceRelativePerformance);

                    i++;
                });
            });

            return relativePerformances;
        }

        private static void GetNeighborsIndexes(int i, int count, out int start, out int end)
        {
            if (count <= 5)
            {
                start = 0;
                end = count - 1;
                return;
            }

            switch (i)
            {
                case 0:
                case 1:
                    start = 0;
                    break;

                default:
                    start = i - 2;
                    break;
            }
            end = start + 4;
            if (end >= count)
            {
                end = count - 1;
                start = end - 4;
            }
        }

        private class DoubleComparer : IComparer<double?>
        {
            private const double difference = 1e-16;
            public int Compare(double? x, double? y)
            {
                var dif = (x.GetValueOrDefault() - y.GetValueOrDefault());

                if (Math.Abs(dif) <= difference)
                    return 0;

                if (dif > difference)
                    return 1;

                return -1;

            }
        }

        private class StringComparer : IComparer<string>
        {
            public int Compare(string s1, string s2)
            {
                return String.Compare(s1, s2, true);
            }
        }

        private Dictionary<KeyValuePair<string, Guid>, List<GovernancePerformance>> GetIssueBenchmarks(string issueNumber, ICollection<GovernanceIssueComputeResult> issueComputeResults)
        {
            IComparer<string> sortkeyComparer = new StringComparer();
            IComparer<double?> scoreComparer = new DoubleComparer();
            Func<GovernanceIssueComputeResult, double?> scoreSelector = x => x.Score;

            var sortKeySelectors = new Dictionary<PerformanceType, Func<GovernanceIssueComputeResult, string>>
            {
                { PerformanceType.PeerGroup, x => x.PeerGroup },
                { PerformanceType.Region, r => r.Region }
            };

            var issueBenchmark = new Dictionary<KeyValuePair<string, Guid>, List<GovernancePerformance>>();
            var comparativeGroupBenchmark = new Dictionary<KeyValuePair<PerformanceType, Guid>, GovernancePerformance>();
            

            foreach (PerformanceType performanceType in Enum.GetValues(typeof(PerformanceType)))
            {
                var sortKeySelector = sortKeySelectors[performanceType];

                var orderedData = issueComputeResults.OrderBy(sortKeySelector, sortkeyComparer)
                    .ThenByDescending(scoreSelector, scoreComparer);

                int currentIndex = 0;
                int count = 0;
                int rank = 1;
                int itemsWithSameRank = 1;
                bool first = true;

                var groupCounts = new Dictionary<string, int>();
                var ranks = new Dictionary<Guid, int>();
                string comparativeGroupKey = null;
                GovernanceIssueComputeResult previousItem = null;

                foreach (var item in orderedData)
                {
                    if (!first)
                    {
                        if (!ItemsInSameComparativeGroup(sortKeySelector, sortkeyComparer, previousItem, item))
                        {
                            comparativeGroupKey = sortKeySelector(previousItem);
                            groupCounts.Add(comparativeGroupKey, count);

                            count = 0;
                            rank = 1;
                            itemsWithSameRank = 1;

                            currentIndex++;
                        }

                        if (ItemsInSameComparativeGroup(sortKeySelector, sortkeyComparer, previousItem, item))
                        {
                            if ((scoreComparer.Compare(scoreSelector(previousItem), scoreSelector(item)) == 0))
                            {
                                itemsWithSameRank++;
                            }
                            else
                            {
                                rank += itemsWithSameRank;
                                itemsWithSameRank = 1;
                            }
                        }
                    }

                    first = false;

                    count++;

                    ranks.Add(item.CompanyId, rank);

                    previousItem = item;
                }

                comparativeGroupKey = sortKeySelector(previousItem);
                groupCounts.Add(comparativeGroupKey, count);

                foreach (var item in orderedData)
                {
                    comparativeGroupKey = sortKeySelector(item);

                    comparativeGroupBenchmark.Add(new KeyValuePair<PerformanceType, Guid>(performanceType, item.CompanyId), 
                        new GovernancePerformance
                        {
                            PerformanceType = performanceType,
                            GroupCount = groupCounts[comparativeGroupKey],
                            Rank = ranks[item.CompanyId],
                            Percentile = ComputePercentile(groupCounts[comparativeGroupKey], ranks[item.CompanyId])
                        });
                }
            }

            foreach (var result in issueComputeResults)
            {
                var regionBenchmark = comparativeGroupBenchmark[new KeyValuePair<PerformanceType, Guid>(PerformanceType.Region, result.CompanyId)];
                var peerGroupBenchmark = comparativeGroupBenchmark[new KeyValuePair<PerformanceType, Guid>(PerformanceType.PeerGroup, result.CompanyId)];

                issueBenchmark.Add(new KeyValuePair<string, Guid>(issueNumber, result.CompanyId),
                    new List<GovernancePerformance> { regionBenchmark, peerGroupBenchmark });
            }


            return issueBenchmark;
        }

        private Dictionary<Guid, GovernancePerformance> GetComparativeGroupBenchmark(PerformanceType performanceType, ICollection<GovernanceComputeResult> collection)
        {
            int currentIndex = 0;
            int count = 0;
            int rank = 1;
            int itemsWithSameRank = 1;
            bool first = true;
            
            Func<GovernanceComputeResult, Guid> keySelector = x => x.CompanyId;
            Func<GovernanceComputeResult, string> sortKeySelector = null;
            Func<GovernanceComputeResult, double?> scoreSelector = x => x.Score;
            IComparer<string> sortkeyComparer = new StringComparer();
            IComparer<double?> scoreComparer = new DoubleComparer();

            if (performanceType == PerformanceType.Region)
                sortKeySelector = x => x.RegionCode;
            if (performanceType == PerformanceType.PeerGroup)
                sortKeySelector = x => x.PeerGroup;

            var ranks = new Dictionary<Guid, int>();
            var groupCounts = new Dictionary<string, int>();

            GovernanceComputeResult previousItem = null;
            string comparativeGroupKey;

            IOrderedEnumerable<GovernanceComputeResult> orderedData;

            orderedData = collection.OrderBy(sortKeySelector, sortkeyComparer)
                .ThenByDescending(scoreSelector, scoreComparer);


            foreach (var item in orderedData)
            {
                if (!first)
                {
                    if (!ItemsInSameComparativeGroup(sortKeySelector, sortkeyComparer, previousItem, item))
                    {
                        comparativeGroupKey = sortKeySelector(previousItem);
                        groupCounts.Add(comparativeGroupKey, count);

                        count = 0;
                        rank = 1;
                        itemsWithSameRank = 1;

                        currentIndex++;
                    }

                    if (ItemsInSameComparativeGroup(sortKeySelector, sortkeyComparer, previousItem, item))
                    {
                        if ((scoreComparer.Compare(scoreSelector(previousItem), scoreSelector(item)) == 0))
                        {
                            itemsWithSameRank++;
                        }
                        else
                        {
                            rank += itemsWithSameRank;
                            itemsWithSameRank = 1;
                        }
                    }
                }

                first = false;

                count++;

                var key = keySelector(item);

                ranks.Add(key, rank);

                previousItem = item;
            }

            comparativeGroupKey = sortKeySelector(previousItem);
            groupCounts.Add(comparativeGroupKey, count);


            var benchmark = new Dictionary<Guid, GovernancePerformance>();
            foreach (var item in orderedData)
            {
                var key = keySelector(item);
                comparativeGroupKey = sortKeySelector(item);

                benchmark.Add(key, new GovernancePerformance
                {
                    PerformanceType = performanceType,
                    GroupCount = groupCounts[comparativeGroupKey],
                    Rank = ranks[key],
                    Percentile = ComputePercentile(groupCounts[comparativeGroupKey], ranks[key])
                });
            }

            return benchmark;
        }



        private double ComputePercentile(int groupCount, int rank)
        {
            return (groupCount > 1) ? (double)(groupCount - rank) * 100 / (groupCount - 1) : 100;
        }

        static bool ItemsInSameComparativeGroup<T>(Func<T, string> sortKeySelector, IComparer<string> sortkeyComparer, T previousItem, T item)
        {
            if (sortKeySelector == null && sortkeyComparer == null)
            {
                return true;
            }
            return sortkeyComparer.Compare(sortKeySelector(previousItem), sortKeySelector(item)) == 0;
        }

        private async Task LogTask(string message, Func<Task> action)
        {
            var sw = new Stopwatch();
            
            await action.Invoke();

            sw.Stop();
            _log.Enqueue($"Processing - {message} in {sw.Elapsed}");
        }

    }
}
